Skip to content

feat(v4): HAD post-fit aggregate() + mode inference; per-level bootstrap-gate convergence (2(b) PR-4, M-027/M-139) - #751

Merged
igerber merged 4 commits into
mainfrom
feat/v4-had-aggregate-convergence
Aug 6, 2026
Merged

feat(v4): HAD post-fit aggregate() + mode inference; per-level bootstrap-gate convergence (2(b) PR-4, M-027/M-139)#751
igerber merged 4 commits into
mainfrom
feat/v4-had-aggregate-convergence

Conversation

@igerber

@igerber igerber commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • HAD [M-027] + workflow twin [new row M-139]: HeterogeneousAdoptionDiD.fit(aggregate=) and did_had_pretest_workflow(aggregate=) deprecate via the shared NOT_SUPPLIED shim. The param was a MODE SELECTOR over mutually exclusive panel shapes, so the sentinel resolves by panel-shape inference (shared _infer_aggregate_mode, running after the column-alias reconciliation): two distinct periods select the overall single-period WAS estimator/pretest battery, more select the event-study ones. A plain multi-period fit() goes error→works; supplied legacy values warn then run the legacy routing unchanged (the pre-existing value validation survives).
  • Post-fit aggregate() on both HAD results classes as PURE VIEWS with per-class supported subsets and NO retained kit (results unpickled from any release aggregate identically): aggregate('simple') on overall fits is a one-row bit-exact relay whose target carries the estimand label (WAS/WAS_d_lower), with n = n_obs contributing units and provenance-exact df; aggregate('event_study') on event-study fits is a build_event_study_surface passthrough (cband fields included). The _from_had adapter's n_kind is corrected "obs""units".
  • AggregationResult.summary() estimand-heading widening: the target column + neutral estimate heading now also render when the single distinct target is not "att" (column sized to the longest label) - previously HAD's WAS and dCDH's estimand-labelled relays would render under a hard-coded ATT heading; uniform-att and ContinuousDiD att/acrt output are byte-stable.
  • Per-level bootstrap-gate convergence [M-020..M-023]: CallawaySantAnna, EfficientDiD, ImputationDiD and TwoStageDiD previously failed closed on every aggregate() level of a bootstrapped fit. Their aggregate('simple') is a bit-exact relay of the stored overall quintet (percentile se/p/CI beside the finite safe_inference t), so it now dispatches before the bootstrap gate and stays available - with a NaN df column, because no df governs percentile inference - while the recompute levels keep failing closed. This supersedes the uniform-conservatism decision recorded with M-021..M-023; the normative per-level rule is added to v4-design §6.
  • Consumer admission: HAD event-study containers stay rejected by compute_honest_did/compute_pretrends_power - DEFERRED, not by-design, with corrected wording: the coefficients ARE reference-normalized (each horizon differences against the F-1 anchor); only the anchor row is omitted from the container, and no joint cross-horizon covariance exists.
  • Ledger/guards: M-027 → shimmed; new row M-139 (121 rows; test_v4_matrix range/count amendments; naming-guard aggregate token + CONSUMER_ALLOWLIST/SURFACE_ALLOWLIST entries with migrate-first discipline); M-092 scope qualifier and M-093 amendment extended to HAD.
  • Sweep: 125 test-site mode-kwarg drops/migrations (incl. the drift-locked T20/T21/T22 fixtures and test_v4_rename_mechanical's no-FutureWarning pins), practitioner/guides/REGISTRY/tutorials/troubleshooting migrations, four unreleased CHANGELOG bullets amended to the per-level policy, re-derived had.py line anchors repo-wide.

Methodology references (required if estimator / math changes)

  • Method name(s): Heterogeneous Adoption Design DiD (WAS estimand); post-fit aggregation contract (v4-design §6).
  • Paper / source link(s): de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026), arXiv:2405.04465 (Section 2, Appendix B.2 - the two-period/multi-period regimes the mode inference follows).
  • Any intentional deviations from the source (and why): None to estimation - no estimator equations, weights, variance formulas, or bootstrap algorithms change (relays are bit-exact; pickles byte-identical pre/post). The bootstrap-gate convergence reverses a previously RECORDED API decision (uniform fail-closed on bootstrapped fits, REGISTRY EDiD/Imputation/TwoStage Notes) - deliberate, documented as superseded in the same Notes, with the original rationale honored via the NaN-df guard. All API-level decisions are documented in the REGISTRY HeterogeneousAdoptionDiD Note (rows M-027/M-139) and the ledger.

Validation

  • Tests added/updated: tests/test_aggregate_contract.py (30-test HAD block: no-warn inference pins per shape, bit-identical plain≡supplied equivalence, degenerate-inference relays, post-filter sentinel regression, workflow twins, summary-heading pins incl. dCDH; the four bootstrap fail-closed pins flipped into relay+recompute pairs with survey+bootstrap arms), tests/test_event_study_consumers.py (HAD rejection pins), tests/test_guides.py (shim-line + no-teach guards), tests/test_t21_had_pretest_workflow_drift.py (notebook prose guard), plus migrations across test_had.py, test_had_pretests.py, test_had_dual_knob_deprecation.py, test_v4_rename_mechanical.py, drift suites, test_v4_matrix.py, test_naming_guard.py, test_practitioner.py.
  • Backtest / simulation / notebook evidence (if applicable): tutorials 15/20/21/22 re-executed via nbmake (numbers unmoved - T20/T21/T22 drift suites green); HAD result pickles byte-identical pre/post (1,399 B overall / 3,304 B event-study - no new retention).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

…rap-gate convergence (2(b) PR-4, M-027/M-139)

fit(aggregate=) and did_had_pretest_workflow(aggregate=) deprecate via
the NOT_SUPPLIED shim. Unlike every prior adopter the param was a MODE
SELECTOR over mutually exclusive panel shapes, so the sentinel resolves
by panel-shape inference (shared _infer_aggregate_mode, running after
the column-alias reconciliation): two distinct periods -> the overall
single-period WAS estimator/battery, more -> the event-study ones. A
plain multi-period fit() goes error->works; supplied legacy values warn
then run the legacy routing unchanged (validation survives). Post-fit
aggregate() lands on both results classes as PURE VIEWS with per-class
supported subsets and NO kit (legacy pickles aggregate identically):
'simple' on overall fits (target = the WAS estimand label, n = n_obs
units, provenance-exact df), 'event_study' on event-study fits (the
_from_had passthrough, cband included; its n_kind corrected obs->units).
summary() renders the target column + neutral estimate heading whenever
the single distinct target is not 'att' (sized to the longest label;
fixes dCDH's shipped ATT-mislabel; uniform-att and att/acrt output
byte-stable). HAD ES containers stay outside honest/pretrends -
DEFERRED wording corrected: coefficients ARE F-1-anchored, only the
anchor ROW is omitted and no joint covariance exists.

Per-level bootstrap-gate convergence (supersedes the recorded uniform
fail-closed decision of M-021..M-023): CS/EDiD/Imputation/TwoStage
aggregate('simple') is a bit-exact relay of the stored overall quintet,
so it now dispatches before the bootstrap gate and stays available on
bootstrapped fits - with a NaN df column (no df governs percentile
inference) - while the recompute levels keep failing closed. Normative
per-level rule added to v4-design section 6.

Ledger: M-027 shimmed; NEW row M-139 (121 rows; matrix + naming-guard
amendments incl. the 'aggregate' token + CONSUMER/SURFACE allowlists);
M-092 qualifier + M-093 amendment extended to HAD; M-020..M-026 note
convergence. Tests: 30-test HAD contract block (inference equivalence
bit-identical per shape, degenerate-inference relays, post-filter
sentinel regression, workflow twins), the four bootstrap pins flipped
into relay+recompute pairs with survey arms, consumer rejection pins,
llms-full shim + no-teach guards, notebook prose drift guard. Sweep:
125 test-site kwarg drops/migrations, practitioner/guides/REGISTRY/
tutorials/troubleshooting/CHANGELOG (incl. four unreleased blanket-
policy bullets), re-derived had.py line anchors. Pickles byte-identical
pre/post (no retention; views are O(1) relays).
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Overall assessment

⚠️ Needs changes — one unmitigated P1 semantic-contract issue.

Executive summary

  • HAD mode inference and bootstrap relay behavior match the Methodology Registry.
  • No estimator, variance, weighting, NaN-propagation, or control-group defect found.
  • The new HAD event-study adapter silently loses the WAS estimand label and renders it as ATT.
  • Existing covariance and df-provenance limitations are properly tracked.
  • Tutorial 22 gives an incorrect explanation for larger post-treatment SEs.
  • Tests could not be executed because the environment lacks pytest and numerical dependencies.

Methodology

P1 — HAD event-study results are mislabeled as ATT

Location: diff_diff/had.py:L1045-L1060, diff_diff/results_base.py:L557-L603, diff_diff/results_base.py:L1185-L1225

Impact: HAD estimates WAS or WAS_d_lower, explicitly not ATT. The new _from_had() adapter does not preserve target_parameter, while EventStudyResults.summary() hardcodes an ATT heading. Serialization also provides no way to distinguish the two HAD estimands. This silently changes the reported meaning of otherwise-correct numbers.

Concrete fix: Add optional estimand metadata to EventStudyResults, populate it from results.target_parameter in _from_had(), and use it in summary() and serialization. Alternatively, render a neutral Estimate heading while retaining the explicit estimand metadata. Add tests for both WAS and WAS_d_lower; the current tests only check relay equality (tests/test_aggregate_contract.py:L3829-L3846).

All other reviewed methodology changes conform to the Registry’s documented M-027/M-139 and per-level bootstrap policies.

Code Quality

No additional findings.

Performance

No findings. The HAD aggregations are pure views, and bootstrap recomputation gates occur before expensive work.

Maintainability

No additional findings.

Tech Debt

P3 — Tracked HAD event-study limitations

Impact: Joint covariance remains unavailable, and survey fits lack per-row df provenance.

Concrete fix: No action required in this PR. These are tracked in DEFERRED.md:L88 and TODO.md:L28.

Security

No findings or apparent secrets.

Documentation/Tests

P2 — Tutorial attributes residual variance to fitted treatment signal

Location: docs/tutorials/22_had_survey_design.ipynb:L508-L518, L618-L627

Impact: In the stated linear DGP, slope * D_{g,F} is fitted conditional-mean signal, not residual variation. Its presence does not itself make the local-linear residual variance larger. The prose teaches an incorrect interpretation of post-treatment SEs.

Concrete fix: Attribute the observed differences to horizon-specific errors, conditional variance, boundary leverage, and survey influence-function clustering. Also replace “cover zero — no pre-trends” with “include zero — no detected pre-trend.”

Path to Approval

  1. Preserve HAD’s WAS/WAS_d_lower metadata through _from_had() and remove the hardcoded ATT presentation, with regression tests.
  2. Correct Tutorial 22’s SE interpretation.

…erpretation prose (P2)

P1: the HAD event-study container rendered WAS/WAS_d_lower numbers
under summary()'s hardcoded ATT heading with no estimand metadata
anywhere on the surface. New optional 'estimand' provenance field
(appended last, the M-092 amendment convention), populated by
_from_had from target_parameter, used as the summary() column heading
(dynamic width) + a metadata-line entry, and serialized by to_dict().
Every other producer keeps estimand=None and renders byte-identically.
Regression tests: WAS + WAS_d_lower heading/metadata/serialization
pins + a non-HAD ATT byte-stability pin.

P2: tutorial 22 attributed larger post-launch per-horizon SEs to the
fitted treatment signal (slope * D), which the local-linear boundary
fit absorbs as conditional-mean structure in this linear DGP; reworded
to the error side (horizon-specific noise and PSU x period shock
draws, conditional variance, boundary leverage, survey IF clustering)
and 'cover zero - no pre-trends' -> 'include zero - no detected
pre-trend'.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: cf1faf0d6a07ae2d4d0df26cc750e95f7605926f


Overall assessment

⚠️ Needs changes — the prior P1 estimand-label issue remains partially unresolved.

Executive summary

  • HAD summaries and to_dict() now preserve WAS/WAS_d_lower.
  • to_dataframe() still silently exports HAD estimates as att without estimand metadata.
  • Mode inference, bootstrap relays, df handling, and NaN gating match the Methodology Registry.
  • The prior Tutorial 22 explanation has been corrected.
  • Tests could not run because NumPy, pandas, SciPy, and pytest are unavailable.

Methodology

P1 — DataFrame export still mislabels HAD’s estimand

Location: diff_diff/results_base.py:L488-L514, diff_diff/results_base.py:L1202-L1244

Impact: aggregate("event_study").to_dataframe() returns a bare att column without estimand or target. Once detached from the result object, WAS and WAS_d_lower estimates are indistinguishable from ATT estimates. This leaves the prior semantic-contract finding only partially fixed.

Concrete fix: Add an estimand/target column to EVENT_STUDY_SCHEMA, populated with self.estimand or "att". Test both HAD estimands and a non-HAD ATT producer. The existing test only checks summary() and to_dict() (tests/test_aggregate_contract.py:L3848-L3859).

All other reviewed methodology changes conform to the documented M-027/M-139 and per-level bootstrap policies.

Code Quality

No additional findings.

Performance

No findings.

Maintainability

No additional findings.

Tech Debt

P3 — Tracked HAD event-study limitations

Impact: Joint covariance remains unavailable, and survey fits lack per-row df provenance.

Concrete fix: No action required in this PR; tracked in DEFERRED.md:L88 and TODO.md:L28.

Security

No findings or apparent secrets.

Documentation/Tests

No additional findings. Tutorial 22 now correctly attributes SE differences to horizon-specific errors, boundary leverage, and survey influence-function clustering.

Path to Approval

  1. Preserve WAS/WAS_d_lower in EventStudyResults.to_dataframe() through an explicit estimand discriminator, with regression tests for both HAD labels and ordinary ATT output.

The detached to_dataframe() frame still exported a bare att column, so
WAS-family numbers became indistinguishable from ATTs once separated
from the container. The pinned schema gains a per-row 'estimand'
discriminator appended last ('att' for every ATT producer; the
WAS/WAS_d_lower label for HAD - the AggregationResult.target
precedent), emitted by to_dataframe and recorded as an M-092 pre-cut
amendment. Regression pins extended to the frame for both HAD labels
and a non-HAD ATT producer.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 31de2577fee54f2169e3377cf352220d910331c3


Overall assessment

Looks good — the prior P1 is resolved; no unmitigated P0/P1 findings remain.

Executive summary

  • HAD event-study DataFrame exports now preserve WAS/WAS_d_lower estimand metadata.
  • Panel-shape mode inference matches the documented M-027/M-139 methodology.
  • Bootstrap simple relays, NaN df handling, and recompute gates follow the Registry.
  • Remaining covariance, df-provenance, and bootstrap-replay limitations are explicitly tracked.
  • Runtime tests were unavailable due to missing dependencies; static parsing and diff checks passed.

Methodology

No unmitigated findings. The HAD mode inference, pure aggregation views, unit counts, and bootstrap behavior conform to the documented Registry note at docs/methodology/REGISTRY.md:L3794.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

P3 — Tracked inference limitations

Impact: HAD lacks joint cross-horizon covariance and per-row survey df provenance; recomputed bootstrap aggregation levels remain unavailable.

Concrete fix: No action required in this PR. These are tracked in DEFERRED.md:L88 and TODO.md:L24-L28, TODO.md:L43.

Security

No findings or apparent secrets.

Documentation/Tests

The previous estimand-label defect is fully addressed:

  • Schema and serialization: diff_diff/results_base.py:L117-L135, L493-L522
  • HAD adapter propagation: diff_diff/results_base.py:L1226-L1252
  • Regression coverage for both HAD labels and ordinary ATT: tests/test_aggregate_contract.py:L3848-L3895

No additional findings. The test suite could not run because NumPy, pandas, SciPy, and pytest are unavailable in the review environment.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 5, 2026
…/ absent

The new test_notebook_prose_has_no_malformed_mode_phrases opened the
tutorial notebooks with bare relative paths, so every CI test leg
failed with FileNotFoundError when pytest ran outside the repo root
(the only failure - 9,485 passed). Resolve via
Path(__file__).parents[1] and pytest.skip when docs/ is absent (the
tests/_tutorial_drift loader convention for the isolated-install job).
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: d3ba9c2878e1837c2c2946ccf03680f88d26ca27


Overall assessment

Looks good — the prior P1 remains resolved; no unmitigated P0/P1 findings.

Executive summary

  • HAD mode inference and post-fit views match the cited Appendix B.2 methodology and Registry note.
  • The prior WAS estimand-label defect remains fully fixed and regression-tested.
  • Bootstrap simple relays correctly preserve stored inference while clearing df provenance.
  • Degenerate bootstrap inference is consistently NaN-gated.
  • Remaining inference limitations are explicitly tracked.

Methodology

No unmitigated findings.

Affected methods are HAD (WAS/WAS_d_lower) and post-fit aggregation for CS, EfficientDiD, ImputationDiD, and TwoStageDiD. Panel-shape routing and last-cohort handling conform to the paper review (docs/methodology/papers/dechaisemartin-2026-review.md:L18, L124, L193) and the documented deviation/extension at docs/methodology/REGISTRY.md:L3794.

The implementation matches these contracts at diff_diff/had.py:L132, diff_diff/had.py:L593, diff_diff/had.py:L1045, and diff_diff/results_base.py:L1226.

Code Quality

No findings. No new inline inference anti-pattern or incomplete parameter propagation was found.

Performance

No findings. HAD aggregation remains a pure view without additional retained estimator state.

Maintainability

No findings. The shared mode-inference helper prevents estimator/workflow routing drift.

Tech Debt

P3 — Tracked inference limitations

Impact: HAD still lacks joint cross-horizon covariance; recomputed bootstrap aggregation levels remain unavailable; several event-study producers lack per-row df provenance.

Concrete fix: No action required in this PR. These limitations are tracked in DEFERRED.md:L88 and TODO.md:L24-L28.

Security

No findings or apparent secrets introduced.

Documentation/Tests

No findings. The prior estimand-label issue is covered by diff_diff/results_base.py:L493-L522, diff_diff/results_base.py:L1226-L1252, and tests/test_aggregate_contract.py:L3848-L3895.

The latest notebook-guard path correction is sound at tests/test_t21_had_pretest_workflow_drift.py:L452-L475.

Runtime tests could not run because NumPy and related dependencies are unavailable. Write-free compilation of 18 changed Python files and JSON parsing of all four changed notebooks succeeded.

@igerber
igerber merged commit 003d215 into main Aug 6, 2026
31 checks passed
@igerber
igerber deleted the feat/v4-had-aggregate-convergence branch August 6, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant